#!/usr/bin/env python3

# Copyright (C) 1994-2021 Altair Engineering, Inc.
# For more information, contact Altair at www.altair.com.
#
# This file is part of both the OpenPBS software ("OpenPBS")
# and the PBS Professional ("PBS Pro") software.
#
# Open Source License Information:
#
# OpenPBS is free software. You can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
# Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# OpenPBS is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Commercial License Information:
#
# PBS Pro is commercially licensed software that shares a common core with
# the OpenPBS software.  For a copy of the commercial license terms and
# conditions, go to: (http://www.pbspro.com/agreement.html) or contact the
# Altair Legal Department.
#
# Altair's dual-license business model allows companies, individuals, and
# organizations to create proprietary derivative works of OpenPBS and
# distribute them - whether embedded or bundled with other software -
# under a commercial license agreement.
#
# Use of Altair's trademarks, including but not limited to "PBS™",
# "OpenPBS®", "PBS Professional®", and "PBS Pro™" and Altair's logos is
# subject to Altair's trademark licensing policies.

import argparse
import configparser
import copy
import fileinput
import json
import os
import platform
import re
import shlex
import shutil
import subprocess
import sys
import textwrap
import threading
import time
from argparse import RawTextHelpFormatter
from string import Template

ci_dirname = ''
default_platform = ''
MACROS = {}


def read_macros():
    for line in open(os.path.join(ci_dirname, 'etc', 'macros')):
        var, value = line.split('=')
        MACROS[var] = value.replace('\n', '')


requirements_template = Template('''num_servers=${num_servers}
num_moms=${num_moms}
num_comms=${num_comms}
no_mom_on_server=${no_mom_on_server}
no_comm_on_server=${no_comm_on_server}
no_comm_on_mom=${no_comm_on_mom}
''')

service_template_prist = Template('''{
"image": "${image}",
"volumes": [
    "../:/pbssrc",
    "./:/src",
    "./logs:/logs",
    "./etc:/workspace/etc"
],
"entrypoint": "/workspace/etc/container-init",
"environment": [
    "NODE_TYPE=${node_type}",
    "LANG=en_US.utf-8"
],
"networks": {
    "ci.local": { }
},
"domainname": "ci.local",
"container_name": "${hostname}",
"hostname": "${hostname}",
"user": "root",
"privileged": true,
"stdin_open": true,
"tty": true
}''')


def log_error(msg):
    print("ERROR ::: " + str(msg))


def log_info(msg):
    t = time.localtime()
    current_time = time.strftime("%H:%M:%S", t)
    print(current_time + " ---> " + str(msg))


def log_warning(msg):
    print("WARNING ::: " + str(msg))


def get_services_list():
    _ps = subprocess.run(
        ["docker-compose", "-f", "docker-compose.json",
         "ps", "--filter", "status=running", "--services"],
        stdout=subprocess.PIPE)
    _p = str((_ps.stdout).decode('utf-8'))
    return [x for x in _p.splitlines() if len(x) > 0]


def get_compose_file_services_list():
    compose_file = os.path.join(ci_dirname, 'docker-compose.json')
    with open(compose_file) as f:
        compose_file = json.loads(f.read())
    return list(compose_file['services'].keys())


def run_cmd(cmd, return_output=False):
    '''
    Run a terminal command, and if needed return output of the command.
    '''
    cmd = shlex.split(cmd)
    try:
        a = subprocess.Popen(cmd, stdout=subprocess.PIPE)
        out, err = a.communicate()
        if a.returncode != 0:
            log_error("command failed")
            log_error(str(err))
        else:
            if return_output:
                return str(out)
    except Exception as e:
        log_error("The command failed.")
        log_error(e)


def run_docker_cmd(run_cmd, run_on='all'):
    '''
    Runs a docker command and on failure redirects user to
    the container terminal
    '''
    services = get_services_list()
    services.sort(reverse=True)  # we want server cmds to run first
    for service in services:
        cmd = "docker-compose -f docker-compose.json exec "
        cmd += service + " bash -c \'" + run_cmd + "\'"
        if run_on != 'all' and service.find(run_on) == -1:
            log_info('Skipping on ' + service +
                     ' as command only to be run on ' + run_on)
            continue
        try:
            log_info(cmd)
            docker_cmd = shlex.split(cmd)
            a = subprocess.Popen(docker_cmd)
            a.communicate()
            if a.returncode != 0:
                _msg = "docker cmd returned with non zero exit code,"
                _msg += "redirecting you to container terminal"
                log_error(_msg)
                _docker_cmd = "docker-compose -f docker-compose.json exec "
                _docker_cmd += service + " bash -c \'cd /pbssrc && /bin/bash\'"
                docker_cmd = shlex.split(_docker_cmd)
                subprocess.run(docker_cmd)
                os._exit(1)
        except Exception as e:
            log_error("Failed\n:")
            log_error(e)


def write_to_file(file_path, value):
    with open(file_path, "w+") as f:
        f.write(value)


def read_from_file(file_path):
    if not os.path.isfile(file_path):
        open(file_path, 'a').close()
    with open(file_path, 'r+') as f:
        val = f.read()
    return val


def commit_docker_image():
    '''
    Watch for readiness of ci containers to commit a new image
    '''

    images_to_commit = {}
    time_spent = 0
    services = get_services_list()
    service_count = len(services)
    timeout = 1 * 60 * 60
    while service_count > 0:
        # Do not want to check constantly as it increases cpu load
        time.sleep(15)
        time_spent = time_spent + 15
        if time_spent > timeout:
            log_error("build is taking too long, timed out")
            sys.exit(1)
        status = read_from_file(os.path.join(
            ci_dirname, MACROS['CONFIG_DIR'], MACROS['STATUS_FILE']))
        for service in services:
            if str(status).find(service) != -1:
                services.remove(service)
                service_count -= 1
                image = (service.split('-', 1)[1][:-2]).replace('-', ':')
                image = image.replace("_", ".")
                images_to_commit[image] = service
    for key in images_to_commit:
        try:
            build_id = 'docker-compose -f docker-compose.json ps -q ' + \
                images_to_commit[key]
            build_id = run_cmd(build_id, True)
            build_id = build_id.split("'")[1]
            build_id = build_id[:12]
            image_name = (str(key).replace(':', '-')
                          ).replace('.', '_') + '-ci-pbs'
            # shortening the build id to 12 characters as is displayed by
            # 'docker ps' unlike 'docker-compose ps'  which shows full id
            cmd = 'docker commit '+build_id+' '+image_name+':latest'
            log_info(cmd)
            run_cmd(cmd)
        except Exception as e:
            log_error(e)
        try:
            bad_images = "docker images -qa -f'dangling=true'"
            bad_images = run_cmd(bad_images, True)
            if bad_images != "b''":
                bad_images = (bad_images.split("'")[1]).replace("\\n", " ")
                print("The following untagged images will be removed -> " +
                      bad_images)
                cmd = 'docker rmi ' + bad_images
                run_cmd(cmd)
        except Exception as e:
            log_warning(
                "could not remove bad (dangling) images, \
                please remove manually")
            print(e)
    return True


def create_ts_tree_json():
    benchpress_opt = os.path.join(
        ci_dirname, MACROS['CONFIG_DIR'], MACROS['BENCHPRESS_OPT_FILE'])
    benchpress_value = read_from_file(benchpress_opt)
    try:
        cmd = '/src/etc/gen_ptl_json.sh "' + benchpress_value + '"'
        run_docker_cmd(cmd, run_on='server')
    except Exception:
        log_error('Failed to generate testsuite info json')
        sys.exit(1)


def get_node_config(node_image=default_platform):
    '''
    Calculate the required node configuration for given
    requirements decorator and return node config
    '''
    json_data = {}
    max_servers_needed = 1
    max_moms_needed = 1
    max_comms_needed = 1
    no_mom_on_server_flag = False
    no_comm_on_mom_flag = True
    no_comm_on_server_flag = False
    try:
        with open(os.path.join(ci_dirname, 'ptl_ts_tree.json')) as f:
            json_data = json.load(f)
    except Exception:
        log_error('Could not find ptl tree json file')
    for ts in json_data.values():
        for tclist in ts['tclist'].values():
            max_moms_needed = max(
                tclist['requirements']['num_moms'], max_moms_needed)
            max_servers_needed = max(
                tclist['requirements']['num_servers'], max_servers_needed)
            max_comms_needed = max(
                tclist['requirements']['num_comms'], max_comms_needed)
            no_mom_on_server_flag = tclist['requirements']['no_mom_on_server']\
                or no_mom_on_server_flag
            no_comm_on_server_flag = tclist['requirements']['no_comm_on_server']\
                or no_comm_on_server_flag
            no_comm_on_mom_flag = tclist['requirements']['no_comm_on_mom']\
                or no_comm_on_mom_flag
    # Create a bash readable requirements decorator file
    write_to_file(os.path.join(ci_dirname, MACROS['CONFIG_DIR'],
                               MACROS['REQUIREMENT_DECORATOR_FILE']),
                  requirements_template.substitute(num_servers=max_servers_needed,
                                                   num_moms=max_moms_needed,
                                                   num_comms=max_comms_needed,
                                                   no_mom_on_server=no_mom_on_server_flag,
                                                   no_comm_on_server=no_comm_on_server_flag,
                                                   no_comm_on_mom=no_comm_on_mom_flag))

    server_nodes = []
    mom_nodes = []
    comm_nodes = []
    # get required number of servers and moms
    for _ in range(max_servers_needed):
        server_nodes.append(node_image)
    if not no_mom_on_server_flag:
        max_moms_needed = max(max_moms_needed, max_servers_needed)
        if max_moms_needed > max_servers_needed:
            for _ in range(max_moms_needed - max_servers_needed):
                mom_nodes.append(node_image)
    else:
        for _ in range(max_moms_needed):
            mom_nodes.append(node_image)

    only_moms = len(mom_nodes)
    # get required num of comms
    if no_comm_on_mom_flag and no_comm_on_server_flag:
        for _ in range(max_comms_needed):
            comm_nodes.append(node_image)
    elif no_comm_on_mom_flag and not no_comm_on_server_flag:
        if max_comms_needed > max_servers_needed:
            for _ in range(max_comms_needed-max_servers_needed):
                comm_nodes.append(node_image)
    else:
        if max_comms_needed > only_moms:
            for _ in range(max_comms_needed - only_moms):
                comm_nodes.append(node_image)

    # remove the trailing ';' from the node_config string
    mom_nodes = ['mom=' + x for x in mom_nodes]
    server_nodes = ['server=' + x for x in server_nodes]
    comm_nodes = ['comm=' + x for x in comm_nodes]
    node_images = ";".join(server_nodes + mom_nodes + comm_nodes)
    return node_images


def tail_build_log():
    server_name = ''
    build_log_path = get_services_list()
    for i in build_log_path:
        if i.find('server') != -1:
            build_log_path = i
            server_name = i
    build_log_path = os.path.join(
        ci_dirname, 'logs', 'build-' + build_log_path)
    prev = ''
    next = ''
    with open(build_log_path, 'rb') as f:
        while True:
            f.seek(-2, os.SEEK_END)
            while f.read(1) != b'\n':
                f.seek(-2, os.SEEK_CUR)
            next = f.readline().decode()
            if next != prev:
                print(next, end='')
                prev = next
            else:
                status = os.path.join(
                    ci_dirname, MACROS['CONFIG_DIR'], MACROS['STATUS_FILE'])
                status = read_from_file(status)
                if status.find(server_name) != -1:
                    return


def check_for_existing_image(val=default_platform):
    '''
    This function will check whether an existing image with the
    post-fix of '-ci-pbs' exists or not for the given docker image.
    '''
    if val.find('-ci-pbs') == -1:
        search_str = val.replace(":", "-")
        search_str = search_str.replace(".", '_')
        search_str += '-ci-pbs'
    cmd = 'docker images -q ' + search_str
    search_result = run_cmd(cmd, True)
    if search_result != "b''":
        return True, search_str
    else:
        return False, val


def get_current_setup():
    '''
    Returns the node config for currently running ci containers
    '''
    compose_file = os.path.join(ci_dirname, 'docker-compose.json')
    node_config = ''
    with open(compose_file) as f:
        compose_file = json.loads(f.read())
    for service in compose_file['services']:
        image = compose_file["services"][service]['image']
        if image[-7:] == '-ci-pbs':
            image = image[:-7][::-1].replace('-', ':', 1)[::-1]
        node_type = compose_file["services"][service]['environment'][0]
        node_type = node_type.split('=')[1]
        node_config += node_type + '=' + image + ';'
    node_config = node_config[:-1]
    return node_config


def load_conf():
    conf_file = os.path.join(
        ci_dirname, MACROS['CONFIG_DIR'], MACROS['CONF_JSON_FILE'])
    with open(conf_file) as f:
        conf_file = json.loads(f.read())
    return conf_file


def show_set_opts():
    conf_opts = load_conf()
    os_file_list = get_compose_file_services_list()
    os_file_list = [(x.split('-', 1)[0] + '=' + x.split('-', 1)[1][:-2]
                     ).replace('-', ':').replace('_', '.')
                    for x in os_file_list]
    os_file_list.sort()
    conf_opts['OS'] = os_file_list
    print(json.dumps(conf_opts, indent=2, sort_keys=True))


def create_param_file():
    '''
    Create param file with necessary node configuration for
    multi node PTL tests.
    '''
    moms = []
    comms = []
    include_server_mom = False
    include_server_comm = False
    include_mom_comm = False
    reqs = read_from_file(os.path.join(
        ci_dirname, MACROS['CONFIG_DIR'],
        MACROS['REQUIREMENT_DECORATOR_FILE']))
    if reqs.find('no_mom_on_server=False') != -1:
        include_server_mom = True
    if reqs.find('no_comm_on_server=False') != -1:
        include_server_comm = True
    if reqs.find('no_comm_on_mom=False') != -1:
        include_mom_comm = True
    for service in get_services_list():
        service = service+'.ci.local'
        if service.find('server') != -1:
            if include_server_mom:
                moms.append(service)
            if include_server_comm:
                comms.append(service)
        if service.find('mom') != -1:
            moms.append(service)
            if include_mom_comm:
                comms.append(service)
        if service.find('comm') != -1:
            comms.append(service)
    write_str = ''
    if len(moms) != 0:
        write_str = 'moms=' + ':'.join(moms) + '\n'
    if len(comms) != 0:
        write_str += 'comms=' + ':'.join(comms)
    param_path = os.path.join(
        ci_dirname, MACROS['CONFIG_DIR'], MACROS['PARAM_FILE'])
    write_to_file(param_path, write_str)


def unpack_node_string(nodes):
    '''
    Helper function to expand abbreviated node config
    '''
    for x in nodes:
        if x.find('*') != -1:
            num = x.split('*')[0]
            try:
                num = int(num)
            except Exception:
                log_error('invalid string provided for "nodes" configuration')
                sys.exit(1)
            val = x.split('*')[1]
            nodes.remove(x)
            for _ in range(num):
                nodes.append(val)
    return ';'.join(nodes)


def build_compose_file(nodes):
    '''
    Build docker-compose file for given node config in function parameter
    '''
    compose_template = {
        "version": "3.5",
        "networks": {
            "ci.local": {
                "name": "ci.local"
            }
        },
        "services": {}
    }
    if nodes.find("*") != -1:
        nodes = unpack_node_string(nodes.split(';'))
    count = 0
    server = ''
    for n in nodes.split(';'):
        count = count + 1
        node_key, node_val = n.split('=')
        if (node_val not in MACROS['SUPPORTED_PLATFORMS'].split(',')
                and ''.join(sys.argv).find(node_val) != -1):
            log_warning("Given platform '" + node_val + "' is not supported by" +
                        " ci, will result in unexpected behaviour")
            log_warning("Supported platforms are " +
                        MACROS['SUPPORTED_PLATFORMS'])
        node_name = node_key + '-' + \
            (node_val.replace(':', '-')).replace('.', '_') + '-' + str(count)
        image_value = node_val
        _, image_value = check_for_existing_image(node_val)
        service_template = json.loads(service_template_prist.substitute(
            image=image_value, node_type=node_key,
            hostname=node_name))
        if node_key == 'server':
            server = node_name
        compose_template['services'][node_name] = service_template
    for service in compose_template['services']:
        compose_template['services'][service]['environment'].append(
            "SERVER="+server)
    f = open(os.path.join(ci_dirname, 'docker-compose.json'), 'w')
    json.dump(compose_template, f, indent=2, sort_keys=True)
    f.close()
    log_info("Configured nodes for ci")


def ensure_ci_running():
    '''
    Check for running ci container; if not start ci container.
    '''
    try:
        service_count = len(get_services_list())
        if service_count == 0:
            log_info("No running service found")
            try:
                log_info('Attempting to start container')
                os.chdir(ci_dirname)
                subprocess.run(["docker-compose", "-f",
                                "docker-compose.json", "down",
                                "--remove-orphans"],
                               stdout=subprocess.DEVNULL)
                if os.path.exists(os.path.join(ci_dirname,
                                               MACROS['CONFIG_DIR'],
                                               MACROS['STATUS_FILE'])):
                    os.remove(os.path.join(
                        ci_dirname, MACROS['CONFIG_DIR'],
                        MACROS['STATUS_FILE']))
                write_to_file(os.path.join(
                    ci_dirname, MACROS['CONFIG_DIR'],
                    MACROS['STATUS_FILE']), '')
                subprocess.run(
                    ["docker-compose", "-f",
                     "docker-compose.json", "up", "-d"])
                log_info('Waiting for container build to complete ')
                build_log_path = os.path.join(ci_dirname, 'logs')
                log_info("Build logs can be found in " + build_log_path)
                # wait for build to complete and commit newly built container
                tail_build_log()
                commit_docker_image()
            except Exception as e:
                log_error(e)
        else:
            log_info("running container found")
            return 0
    except Exception:
        log_error(e)


def check_prerequisites():
    '''
    This function will check whether docker docker-compose commands
    are available. Also check docker version is minimum required.
    '''
    cmd = "where" if platform.system() == "Windows" else "which"

    try:
        subprocess.run([cmd, "docker"], stdout=subprocess.DEVNULL)
    except Exception:
        log_error("docker not found in PATH")
        sys.exit(1)

    def version_tuple(s: str):
        return tuple(int(x) for x in s.split("."))

    try:
        version = subprocess.run(
            ["docker", "--version"], stdout=subprocess.PIPE)
        version = re.findall(r'\s*([\d.]+)', version.stdout.decode('utf-8'))
        req_version = MACROS['REQ_DOCKER_VERSION']
        if version_tuple(version[0]) < version_tuple(req_version):
            print(version[0])
            print("Docker version less than minimum required " + req_version)
            sys.exit(1)
    except Exception:
        log_error("Failed to get docker version")
        sys.exit(1)

    try:
        subprocess.run([cmd, "docker-compose"], stdout=subprocess.DEVNULL)
    except Exception:
        log_error("docker-compose not found in PATH")
        sys.exit(1)


def is_restart_required():
    '''
    This function checks if the number of nodes currently running meet
    requirement for the given test case. If not builds new docker-compose file
    and returns bool value to restart ci.
    '''
    create_ts_tree_json()
    current_file_services_list = get_compose_file_services_list()
    current_node_image = current_file_services_list[0].split(
        '-', 1)[1][:-2].replace('-', ':')
    node_config = get_node_config(node_image=current_node_image)
    potential_list = []
    for val in node_config.split(';'):
        val = val.replace('=', '-')
        val = val.replace(':', '-')
        potential_list.append(val)
    current_file_services_list = [i[:-2] for i in current_file_services_list]
    # compare without platform names
    current_file_services_list = [
        i.split('-', 1)[0] for i in current_file_services_list]
    potential_list = [i.split('-', 1)[0] for i in potential_list]
    potential_list.sort()
    current_file_services_list.sort()
    if current_file_services_list != potential_list:
        build_compose_file(node_config)
        return True
    else:
        return False


def setup_config_dir():
    '''
    Initializes config directory and files for ci
    '''
    command_path = os.path.join(ci_dirname, MACROS['CONFIG_DIR'])
    if not os.path.exists(command_path):
        os.mkdir(command_path)
    target_path = os.path.join(command_path, MACROS['CONF_JSON_FILE'])
    if not os.path.exists(target_path):
        value = '{ "configure": "--prefix=/opt/pbs '
        value += '--enable-ptl", "tests" : "-t SmokeTest" }'
        write_to_file(target_path, value)
    target_path = os.path.join(command_path, MACROS['CONFIGURE_OPT_FILE'])
    if not os.path.exists(target_path):
        value = "--prefix=/opt/pbs --enable-ptl"
        write_to_file(target_path, value)
    target_path = os.path.join(command_path, MACROS['BENCHPRESS_OPT_FILE'])
    if not os.path.exists(target_path):
        value = "-t SmokeTest"
        write_to_file(target_path, value)
    target_path = os.path.join(ci_dirname, 'docker-compose.json')
    if not os.path.exists(target_path):
        build_compose_file('server=' + default_platform)
        run_cmd('docker-compose -f docker-compose.json down --remove-orphans')


def delete_ci():
    '''
    Takes backup of logs and deletes running containers.
    '''
    services = get_services_list()
    if len(services) != 0:
        build_compose_file(nodes=get_current_setup())
        cmd = '/src/etc/killit.sh backup'
        run_docker_cmd(cmd, run_on='server')
        log_warning('Removed logs file')
        log_info('backup files can be found in ' + build_log_path)
    else:
        log_info('No running container found, nothing to backup')
    try:
        os.chdir(ci_dirname)
        run_cmd(
            "docker-compose -f docker-compose.json down --remove-orphans")
        log_info(
            "done delete container and services")
    except Exception as e:
        log_error("Failed to destroy container and services: " + e)


def parse_params(params_list):
    '''
    Update given params
    '''
    if params_list[0] != 'called':
        container_running = False
        conf_opts = load_conf()
        for set_opts in params_list:
            key, value = (set_opts).split('=', 1)
            service_count = len(get_services_list())
            if service_count > 0:
                container_running = True
            if key.lower() == 'nodes':
                if container_running:
                    log_warning(
                        "Deleting existing containers first,\
                        find backup in logs folder")
                    delete_ci()
                build_compose_file(value)
            elif key.lower() == 'os':
                if container_running:
                    log_warning(
                        "Deleting existing containers first, \
                        find backup in logs folder")
                    delete_ci()
                node_string = value.replace('"', '')
                node_string = 'server=' + node_string
                build_compose_file(node_string)
            else:
                if key in conf_opts:
                    conf_opts[key] = value
                    f = open(os.path.join(
                        ci_dirname, MACROS['CONFIG_DIR'],
                        MACROS['CONF_JSON_FILE']), 'w')
                    json.dump(conf_opts, f, indent=2, sort_keys=True)
                    f.close()
                else:
                    log_error("Unrecognised key in parameter: '" +
                              key + "' , nothing updated")
                    sys.exit(1)


def run_ci_local(local):
    '''
    Run ci locally on host without spawning containers
    '''
    os.chdir(ci_dirname)
    # using subprocess.run instead of run_cmd function
    # so we dont supress stdout and stderr
    if local == 'normal':
        exit_code = subprocess.run("./etc/do.sh")
        sys.exit(exit_code.returncode)
    if local == 'sanitize':
        exit_code = subprocess.run("./etc/do_sanitize_mode.sh")
        sys.exit(exit_code.returncode)


def run_ci(build_pkgs=False):
    '''
    Run PBS configure, install PBS and run PTL tests, if build_pkgs
    is set to True it will instead run package build script only
    '''
    # Display Current options
    log_info("Running ci with the following options")
    show_set_opts()
    if len(get_services_list()) > 0:
        build_compose_file(get_current_setup())
    ret = ensure_ci_running()
    if ret == 1:
        log_error(
            "container build failed, build logs can be found in " +
            build_log_path)
        sys.exit(1)
    command_path = os.path.join(ci_dirname, MACROS['CONFIG_DIR'])
    conf_opts = load_conf()
    if build_pkgs:
        build_cmd = '/src/etc/build-pbs-packages.sh'
        log_info('The package build logs can be found in logs/pkglogs')
        run_docker_cmd(build_cmd + ' | tee /logs/pkglogs',
                       run_on='server')
        sys.exit(0)
    if conf_opts['tests'] != '':
        target_path = os.path.join(command_path, MACROS['BENCHPRESS_OPT_FILE'])
        write_to_file(target_path, conf_opts['tests'])
        if is_restart_required():
            delete_ci()
            ensure_ci_running()
    target_path = os.path.join(command_path, MACROS['CONFIGURE_OPT_FILE'])
    if conf_opts['configure'] != read_from_file(target_path):
        write_to_file(target_path, conf_opts['configure'])
        cmd = ' export ONLY_CONFIGURE=1 && /src/etc/do.sh 2>&1 \
            | tee -a /logs/build-$(hostname -s) '
        run_docker_cmd(cmd)
    cmd = ' export ONLY_REBUILD=1 && /src/etc/do.sh 2>&1 \
        | tee -a /logs/build-$(hostname -s) '
    run_docker_cmd(cmd)
    cmd = ' export ONLY_INSTALL=1 && /src/etc/do.sh 2>&1 \
        | tee -a /logs/build-$(hostname -s) '
    run_docker_cmd(cmd)
    target_path = os.path.join(command_path, MACROS['BENCHPRESS_OPT_FILE'])
    if conf_opts['tests'] == '':
        write_to_file(target_path, conf_opts['tests'])
        log_warning("No tests assigned, skipping PTL run")
    else:
        create_param_file()
        write_to_file(target_path, conf_opts['tests'])
        cmd = 'export RUN_TESTS=1 && export ONLY_TEST=1 && /src/etc/do.sh '
        run_docker_cmd(cmd, run_on='server')


if __name__ == "__main__":

    ci_dirname = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    ci_dirname = os.path.join(ci_dirname, 'ci')
    os.chdir(ci_dirname)
    read_macros()
    _help = '''
    Examples of using arguments.
        ./ci -p 'OS=centos:7'
        ./ci -p 'tests=-t SmokeTest'
        ./ci -p 'configure=CFLAGS="-g -O2" --enable-ptl'
        ./ci -p 'nodes=mom=centos:7;server=ubuntu:16.04'
        ./ci -d or ./ci --delete
        ./ci -b or ./ci --build
        ./ci -l or ./ci --local
    Note: Set tests as empty if you dont want to run PTL'
    '''
    _help += 'Supported platforms are ' + MACROS['SUPPORTED_PLATFORMS']
    ap = argparse.ArgumentParser(prog='ci',
                                 description='Runs the ci tool for pbs',
                                 formatter_class=argparse.RawTextHelpFormatter,
                                 epilog=textwrap.dedent(_help),
                                 conflict_handler='resolve')
    _help = 'set configuration values for os | nodes | configure | tests'
    ap.add_argument('-p', '--params', nargs='+',
                    action='append', help=_help, metavar='param')
    _help = 'destroy pbs container'
    ap.add_argument('-d', '--delete', action='store_true', help=_help)
    _help = 'build packages for the current platform.'
    ap.add_argument('-b', '--build-pkgs', nargs='?', const='called',
                    help=_help)
    _help = 'Simply run the tests locally, without spawning any containers.'
    _help += '\ntype can be one of normal (default) or sanitize'
    ap.add_argument('-l', '--local', nargs='?', const='normal',
                    help=_help, metavar='type')
    args = ap.parse_args()
    build_pkgs = False
    default_platform = MACROS['DEFAULT_PLATFORM']
    build_log_path = os.path.join(ci_dirname, 'logs')
    not_local_run = sys.argv.count('-l') == 0 \
        and sys.argv.count('--local') == 0 \
        and sys.argv.count('-l=sanitize') == 0\
        and sys.argv.count('--local=sanitize') == 0 \
        and sys.argv.count('-l=normal') == 0 \
        and sys.argv.count('--local=normal') == 0
    if not_local_run:
        setup_config_dir()
        check_prerequisites()
    if (not args.delete) and not_local_run and (args.params is None):
        ret = ensure_ci_running()
        if ret == 1:
            log_error(
                "container build failed, build logs can be found in " +
                build_log_path)
            sys.exit(1)
    try:
        if args.params is not None:
            for p in args.params:
                parse_params(p)
        if args.build_pkgs is not None:
            build_pkgs = True
        if args.delete is True:
            confirm = input(
                'Are you sure you want to delete containers (Y/N)?: ')
            if confirm[0].lower() == 'n':
                sys.exit(0)
            elif confirm[0].lower() == 'y':
                delete_ci()
            else:
                log_error("Invalid option provided")
            sys.exit(0)
        if args.local is not None:
            run_ci_local(args.local)
    except Exception as e:
        ap.print_help()
        log_error(e)

    run_ci(build_pkgs)
